#       In The Mood - Rhythmbox predictive playback plugin
#       
#       Copyright 2008 Charlotte Curtis <c.f.curtis@gmail.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

import rb
import rhythmdb
import gobject
import gnomevfs
import gconf
import os
import gtk
import subprocess
import signal
import pickle
from InTheMoodPrefsDialog import InTheMoodPrefsDialog

class InTheMoodPlugin (rb.Plugin):
	def __init__(self):
		rb.Plugin.__init__(self)
	
	def activate(self, shell):
		# we'll need to use the shell in other functions
		self.shell = shell
		
		# create the folder containing the feature vectors
		self.vecfolder = os.path.join(os.getenv('HOME'), '.gnome2','rhythmbox', 'inthemood')
		if not os.path.isdir(self.vecfolder):
			os.mkdir(self.vecfolder)
		
		# read in the blacklist of songs
		if os.path.exists(os.path.join(self.vecfolder, '.blacklist')):
			blfile = open(os.path.join(self.vecfolder, '.blacklist'), 'r')
			self.blacklist = pickle.load(blfile)
			blfile.close()
		else:
			self.blacklist = []

		# set variables to use while plugin is running
		self.scan_query_model = None
		self.scan_iter = None
		self.scan_queue = []
		self.scan_running = False
		self.in_queue = []
		self.seed_vecfile = None
		self.scan_paused = True
		
		# the extractor object
		self.fextract = None
		
		# set up gconf and check parameters
		self.gconfclient = gconf.client_get_default()
		self.gconfpath = '/apps/rhythmbox/plugins/inthemood/'
		
		# connect signals to scan new songs and remove deleted
		self.db = shell.props.db
		self.db.connect('entry-added', self.add_song)
		#self.db.connect('entry-deleted', self.delete_song)
		self.db.connect('load-complete', self.db_loaded)
		self.entry_type = self.db.entry_type_get_by_name('song')

		# connect song-changed signals
		self.player = shell.props.shell_player
		self.player.connect('playing-song-changed', self.playing_song_changed)
		
		# create the In The Mood play source
		self.source = gobject.new(rb.StaticPlaylistSource, shell=shell,name=_("In The Mood"), \
			source_group=rb.rb_source_group_get_by_name('playlists'), \
			plugin=self, entry_type=self.entry_type)
		width, height = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR)
		self.source.set_property('icon', gtk.gdk.pixbuf_new_from_file_at_size(\
			'/usr/share/pixmaps/gnome-aorta.png', width, height))
		self.shell.append_source(self.source, None)
		
		# connect the playlist specific signals
		self.source.get_entry_view().connect('entry-deleted', self.entry_removed)
		self.source.get_entry_view().connect('entry-added', self.start_playback)
		
	
	def deactivate(self, shell):
		# Update gconf settings
		self.gconfclient.set_bool(self.gconfpath + 'scan_complete', self.scan_complete)
		self.gconfclient.set_bool(self.gconfpath + 'do_popup', self.do_popup)
		
		# stop scanning after the currently running analysis process
		self.scan_paused = True
		
		# Update blacklist of unscannable songs
		if len(self.blacklist) > 0:
			blfile = open(os.path.join(self.vecfolder, '.blacklist'), 'w')
			pickle.dump(self.blacklist, blfile)
			blfile.close()
			
		del self.shell
		del self.vecfolder
		del self.gconfclient
		del self.gconfpath
		del self.scan_complete
		del self.db
		del self.blacklist
		self.source.delete_thyself()
	
	def create_configure_dialog(self, dialog=None):
		if not dialog:
			glade_file = self.find_file('inthemood-prefs.glade')
			dialog = InTheMoodPrefsDialog(glade_file, self).get_dialog()
		dialog.present()
		return dialog
	
	def update_prefs(self):
		self.scan_complete = self.gconfclient.get_bool(self.gconfpath + 'scan_complete')
		self.do_popup = self.gconfclient.get_bool(self.gconfpath + 'do_popup')
		self.scan_on_add = self.gconfclient.get_bool(self.gconfpath + 'scan_on_add')
		self.evolve = self.gconfclient.get_bool(self.gconfpath + 'evolve')
		
		if self.scan_complete == None:
			self.scan_complete = False
		
		if self.do_popup == None:
			self.do_popup = True
		
		if self.scan_on_add == None:
			self.scan_on_add = False
		
		if self.evolve == None:
			self.evolve = False
			
	def scan_library(self):
		print 'Scanning Library'
		library = self.shell.props.library_source
		self.scan_query_model = library.props.base_query_model
		self.scan_iter = self.scan_query_model.get_iter_first()
		self.scan_next_song()
	
	def add_song(self, tree, entry):
		if self.scan_paused is True:
			return

		# ignore non-library entries
		if entry.get_entry_type() != self.entry_type:
			return
		
		# ignore songs loaded at initial db load
		if not self.scan_on_add:
			return
		
		print "Adding %s to scan queue" % (self.db.entry_get(entry, rhythmdb.PROP_ARTIST) + \
			" - " + self.db.entry_get(entry, rhythmdb.PROP_TITLE))
		self.scan_queue.append(entry)
		self.scan_next_song()
	
	def popup_response_cb(self, dialog, response_id, askagain):
		self.do_popup = not askagain.get_active()
		if response_id == gtk.RESPONSE_NO:
			self.scan_paused = True
		elif response_id == gtk.RESPONSE_YES:
			self.scan_paused = False
			self.scan_library()
		dialog.destroy()

	def db_loaded(self, tree):
		self.update_prefs()
		if not self.scan_complete and self.do_popup:
			message = _("The music feature database is incomplete\n"\
				+ "Would you like to analyze your library now?\n"\
				+ "You can start analysis manually through plugin preferences")
			dia = gtk.Dialog("In The Mood", None, \
				gtk.DIALOG_DESTROY_WITH_PARENT,\
				(gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_YES, gtk.RESPONSE_YES))
			dia.vbox.set_spacing(10)
			hbox = gtk.HBox(False, 10)
			hbox.pack_start(gtk.image_new_from_stock(gtk.STOCK_DIALOG_INFO,gtk.ICON_SIZE_DIALOG))
			hbox.pack_start(gtk.Label(message))
			dia.vbox.pack_start(hbox)
			askagain = gtk.CheckButton(_("Don't ask me again"), False)
			dia.vbox.pack_start(askagain)

			dia.connect('response', self.popup_response_cb, askagain)
			dia.show_all()

	def scan_next_song(self):
		if self.scan_paused or self.scan_running:
			return

		while self.scan_running == False:
			if not self.scan_complete and self.scan_query_model is not None:
				entry = self.scan_query_model.get(self.scan_iter, 0)[0]
				self.scan_iter = self.scan_query_model.iter_next(self.scan_iter)
				if self.scan_iter is None:
					print "Scan complete"
					self.scan_complete = True

			elif len(self.scan_queue) > 0:
				entry = self.scan_queue.pop(0)
			else:
				print "Nothing to scan"
				return

			self.scan_running = self.scan_song(entry)
	
	def timeout(self, pid, vecfile):
		print "Timed out"
		self.blacklist_song(vecfile)
		self.child_timeout_id = 0
		os.kill(pid,signal.SIGTERM)
		return False

	def blacklist_song(self, vecfile):
		print "Blacklisting " + vecfile
		# delete the vecfile if it was created and add the song to the blacklist
		if os.path.exists(vecfile):
			os.remove(vecfile)
		self.blacklist.append(vecfile)
	
	def read_fextract_output(self, source, condition, vecfile):
		if self.child_timeout_id != 0:
			gobject.source_remove(self.child_timeout_id)
			self.child_timeout_id = 0
		if 'MRS_' in source.readlines():
			self.blacklist_song(vecfile)
		self.fextract.wait()
		self.scan_running = False
		self.scan_next_song()
		return False
	
	def scan_song(self, entry):
# TODO: put some kind of gui notification that analysis is occuring
# along with a button to pause analysis
		mimetype = gnomevfs.get_mime_type(entry.get_playback_uri())
		if ('mpeg' not in mimetype) and ('ogg' not in mimetype) and ('wav' not in mimetype):
			return False
		vecfile = os.path.join(self.vecfolder, self.escape_vecfile_name(entry))
		songfile = gnomevfs.unescape_string_for_display(gnomevfs.URI(entry.get_playback_uri()).path)
		if os.path.exists(vecfile) or (vecfile in self.blacklist):
			return False
		else:
			print "Analyzing " + vecfile
			try:
				self.fextract = subprocess.Popen([self.find_file('fextract'), songfile, \
					vecfile], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
				self.child_timeout_id = gobject.timeout_add(5000, self.timeout, self.fextract.pid, vecfile)
				gobject.io_add_watch(self.fextract.stdout, gobject.IO_IN | gobject.IO_HUP,\
					self.read_fextract_output, vecfile)
				os.waitpid(self.fextract.pid, os.WNOHANG)
				return True
			except OSError, e:
				print str(e)
				return False
	
	def escape_vecfile_name(self, entry):
		songname = self.db.entry_get(entry, rhythmdb.PROP_ARTIST) + "-%-" + \
			self.db.entry_get(entry, rhythmdb.PROP_TITLE)
		
		# get rid of any unfortunate slashes
		songname = songname.replace('/','-#-').replace('\\','-*-') + '.vec'
		return songname
		
	def entry_removed(self, entryview, entry):
		try:
			self.in_queue.remove(self.escape_vecfile_name(entry))
		except ValueError:
			print "Exception: " + self.escape_vecfile_name(entry) + " not in queue"
			self.in_queue = []
		return False
		
	def start_playback(self, entryview, entry):
		if not self.in_queue == []:
			return False
		
		vecfile = self.escape_vecfile_name(entry)
		
		# Can't do analysis on songs in blacklist
		if vecfile in self.blacklist:
			self.source.remove_entry(entry)
			print "That song cannot be analyzed, please choose another"
			return False
		
		if not self.evolve:
			self.seed_vecfile = vecfile
			print self.seed_vecfile
		self.in_queue.append(vecfile)
		self.player.set_playing_source(self.source)
		self.shell.props.sourcelist.select(self.source)
		self.source.get_entry_view().select_entry(entry)
		self.player.play_entry(entry)
		
	def playing_song_changed(self, player, entry):
		if not entry:
			# This signal seems to get emitted without any entry playing sometimes
			return False
		
		vecfile = self.escape_vecfile_name(entry)
			
		# Only add a new song if it is the last in the queue and playing from 
		# the in the mood source.
		if (self.in_queue == [] or vecfile == self.in_queue[-1]) \
			 and self.player.get_playing_source() == self.source:
			# If we're not in evolution mode, find the next song based on the seed
			if self.evolve:
				self.find_matching_track(vecfile)
			else:
				self.find_matching_track(self.seed_vecfile)
			return False
	
	def add_to_playlist(self, entry):
		self.source.add_entry(entry, -1)
		vecfile = self.escape_vecfile_name(entry)
		self.in_queue.append(vecfile)
	
	def read_fcompare_output(self, source, condition, vecfile, fcompare):
		best_match = source.readlines()
		print best_match
		best_match = best_match[-1]
		
		# parse from vecfile back to song format (seems clumsy, but it'll work for most cases)
		best_match = os.path.splitext(best_match.replace('-#-', '/').replace('-*-', '\\'))[0].split('-%-')
		
		# from Alexandre Rosenfeld's last.fm queue plugin
		query = self.db.query_new()
		self.db.query_append(query, (rhythmdb.QUERY_PROP_EQUALS, rhythmdb.PROP_ARTIST, \
			best_match[0], rhythmdb.QUERY_PROP_EQUALS, rhythmdb.PROP_TITLE, best_match[1]))
		query_model = self.db.query_model_new_empty()
		self.db.do_full_query_parsed(query_model, query)
		
		# if there's a few different versions, just pick the first
		self.add_to_playlist(query_model.get(query_model.get_iter_first(), 0)[0])
		fcompare.wait()
		return False

	def find_matching_track(self, vecfile):
		print self.in_queue
		fcompare = subprocess.Popen([self.find_file('fcompare'), self.vecfolder, vecfile]\
			+ self.in_queue, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
		gobject.io_add_watch(fcompare.stdout, gobject.IO_IN | gobject.IO_HUP,\
			self.read_fcompare_output, vecfile, fcompare)
		return True
